package org.ring.csr.logger;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;

import org.ring.csr.core.CheckNode;
import org.ring.csr.core.CheckNodeTree;
import org.ring.csr.core.CheckNodeTree.TreeItem;
import org.ring.csr.core.CheckReport;
import org.ring.csr.core.ErrorReport;
import org.ring.csr.core.ExceptionReport;

import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.AuditListener;
import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;

/**
 * 
 * @author Ray Ling
 */
public class RichLogger extends AutomaticBean implements AuditListener {

    /** decimal radix */
    private static final int BASE_10 = 10;

    /** hex radix */
    private static final int BASE_16 = 16;

    /** some known entities to detect */
    private static final String[] ENTITIES = { "gt", "amp", "lt", "apos",
	    "quot", };

    private PrintWriter output = new PrintWriter(System.out);
    private Map<String, CheckNodeTree> checkNodeTrees = new HashMap<String, CheckNodeTree>();
    private Map<String, CheckNode> checkNodes = new HashMap<String, CheckNode>();
    private boolean listAllFiles = false;

    public RichLogger(OutputStream os) {
	output = new PrintWriter(os);
    }

    public void setOutput(String fileName) throws FileNotFoundException {
	final OutputStream out = new FileOutputStream(fileName);
	output = new PrintWriter(out);
    }

    public void setListAllFiles(boolean value) {
	listAllFiles = true;
    }

    public void auditStarted(AuditEvent aEvt) {
	output.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");

	final ResourceBundle compilationProperties = ResourceBundle
		.getBundle("checkstylecompilation");
	final String version = compilationProperties
		.getString("checkstyle.compile.version");

	output.println("<checkstyle version=\"" + version + "\" reporter=\""
		+ getClass().getCanonicalName() + "\">");
    }

    public void auditFinished(AuditEvent aEvt) {
	for (Map.Entry<String, CheckNodeTree> tree : checkNodeTrees.entrySet()) {
	    output.println("<checktype type=\"" + encode(tree.getKey()) + "\">");

	    String base = tree.getValue().removeBasePath();
	    for (TreeItem item : tree.getValue()) {
		if (item.isTreeEnd()) {
		    output.println("</folder>");
		} else if (item.isTreeStart()) {
		    output.println("<folder name=\"" + encode(item.getPath())
			    + "\">");
		} else if (item.isItem()) {
		    String itemName = item.getNode().getPath();
		    itemName = itemName.substring(itemName
			    .lastIndexOf(File.separator) + 1);
		    itemName = itemName.substring(0, itemName.lastIndexOf('.'));
		    output.println("<file type=\""
			    + encode(item.getNode().getType())
			    + "\" class=\""
			    + encode(itemName)
			    + "\" path=\""
			    + encode(item.getPath())
			    + "\" name=\""
			    + encode(item.getNode().getPath().replace(base, ""))
			    + "\">");

		    for (CheckReport r : item.getNode()) {
			if (r instanceof ErrorReport) {
			    ErrorReport report = (ErrorReport) r;
			    output.print("<error" + " line=\""
				    + report.getLine() + "\"");
			    if (report.getColumn() > 0) {
				output.print(" column=\"" + report.getColumn()
					+ "\"");
			    }
			    output.print(" severity=\""
				    + report.getSeverity().getName() + "\"");
			    output.print(" message=\""
				    + encode(report.getMessage().replace(base,
					    "")) + "\"");
			    output.print(" source=\""
				    + encode(report.getSourceName().substring(
					    report.getSourceName().lastIndexOf(
						    '.') + 1)) + "\"");
			    output.println(" fullsource=\""
				    + encode(report.getSourceName()) + "\"/>");

			} else if (r instanceof ExceptionReport) {
			    ExceptionReport report = (ExceptionReport) r;
			    final StringWriter sw = new StringWriter();
			    final PrintWriter pw = new PrintWriter(sw);
			    pw.println("<exception>");
			    pw.println("<![CDATA[");
			    report.getThrowable().printStackTrace(pw);
			    pw.println("]]>");
			    pw.println("</exception>");
			    pw.flush();
			    output.println(encode(sw.toString()));
			}
		    }
		    output.println("</file>");
		}
	    }
	    output.println("</checktype>");
	}

	output.println("</checkstyle>");
	output.flush();
	output.close();
    }

    public void fileStarted(AuditEvent aEvt) {
	String type = getType(aEvt.getFileName());
	checkNodes.put(aEvt.getFileName(),
		new CheckNode(type, aEvt.getFileName()));
    }

    public void fileFinished(AuditEvent aEvt) {
	CheckNode currentNode = checkNodes.get(aEvt.getFileName());
	if (listAllFiles || currentNode.size() > 0) {
	    CheckNodeTree tree = checkNodeTrees.get(currentNode.getType());
	    if (null == tree) {
		tree = new CheckNodeTree(File.separator);
		checkNodeTrees.put(currentNode.getType(), tree);
	    }

	    tree.add(currentNode);
	}
    }

    public void addError(AuditEvent aEvt) {
	CheckNode currentNode = checkNodes.get(aEvt.getFileName());
	if (!SeverityLevel.IGNORE.equals(aEvt.getSeverityLevel())) {
	    ErrorReport report = new ErrorReport(aEvt.getLine(),
		    aEvt.getColumn(), aEvt.getMessage(),
		    aEvt.getSeverityLevel(), aEvt.getSource(),
		    aEvt.getSourceName());
	    currentNode.addReport(report);
	}
    }

    public void addException(AuditEvent aEvt, Throwable aThrowable) {
	CheckNode currentNode = checkNodes.get(aEvt.getFileName());
	ExceptionReport report = new ExceptionReport(aEvt.getSource(),
		aThrowable);
	currentNode.addReport(report);
    }

    /**
     * Escape &lt;, &gt; &amp; &apos; and &quot; as their entities.
     * 
     * @param aValue
     *            the value to escape.
     * @return the escaped value if necessary.
     */
    public String encode(String aValue) {
	final StringBuffer sb = new StringBuffer();
	for (int i = 0; i < aValue.length(); i++) {
	    final char c = aValue.charAt(i);
	    switch (c) {
	    case '<':
		sb.append("&lt;");
		break;
	    case '>':
		sb.append("&gt;");
		break;
	    case '\'':
		sb.append("&apos;");
		break;
	    case '\"':
		sb.append("&quot;");
		break;
	    case '&':
		final int nextSemi = aValue.indexOf(";", i);
		if ((nextSemi < 0)
			|| !isReference(aValue.substring(i, nextSemi + 1))) {
		    sb.append("&amp;");
		} else {
		    sb.append('&');
		}
		break;
	    default:
		sb.append(c);
		break;
	    }
	}
	return sb.toString();
    }

    /**
     * @return whether the given argument a character or entity reference
     * @param aEnt
     *            the possible entity to look for.
     */
    public boolean isReference(String aEnt) {
	if (!(aEnt.charAt(0) == '&') || !aEnt.endsWith(";")) {
	    return false;
	}

	if (aEnt.charAt(1) == '#') {
	    int prefixLength = 2; // "&#"
	    int radix = BASE_10;
	    if (aEnt.charAt(2) == 'x') {
		prefixLength++;
		radix = BASE_16;
	    }
	    try {
		Integer.parseInt(
			aEnt.substring(prefixLength, aEnt.length() - 1), radix);
		return true;
	    } catch (final NumberFormatException nfe) {
		return false;
	    }
	}

	final String name = aEnt.substring(1, aEnt.length() - 1);
	for (String element : ENTITIES) {
	    if (name.equals(element)) {
		return true;
	    }
	}
	return false;
    }

    public String getType(String fileName) {
	int lastIdx = fileName.lastIndexOf('.');
	if (lastIdx >= 0 && lastIdx + 1 < fileName.length()) {
	    return fileName.substring(lastIdx + 1);
	} else {
	    return "file";
	}
    }
}
